package biz

import (
	"context"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"google.golang.org/protobuf/types/known/timestamppb"
	"strconv"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/golang-jwt/jwt/v4"

	"gitee.com/shuokeyun/kratos/errors"
	pb "gitee.com/shuokeyun/merchant-service/api/v1"
	"gitee.com/shuokeyun/merchant-service/internal/domain"
)

var ErrInvalidToken = pb.ErrorUnauthenticated("无效的token")

type userAuthUsecase struct {
	userRepo    domain.MerchantUserRepo
	redisClient *redis.Client
}

func NewUserAuthUsecase(userRepo domain.MerchantUserRepo, client *redis.Client) domain.UserAuthUsecase {
	return &userAuthUsecase{
		userRepo:    userRepo,
		redisClient: client,
	}
}

func (u *userAuthUsecase) Login(ctx context.Context, mobile string, password string, appId int32) (*domain.UserAuth, error) {
	user, err := u.userRepo.GetMerchantUserByMobile(ctx, mobile, []string{"id", "password", "salt", "merchant_id", "username", "real_name", "role"})
	if err != nil {
		if errors.IsRecordNotFound(err) {
			return nil, pb.ErrorUsernameOrPasswordError("用户名或密码错误")
		}
		return nil, err
	}
	if !checkPassword(password, user.Password, user.Salt) {
		return nil, pb.ErrorUsernameOrPasswordError("用户名或密码错误")
	}
	expireAt := time.Now().Add(time.Hour * 24)
	token := generateToken(user.Id, appId, expireAt)
	userAuth := &domain.UserAuth{
		Id:         user.Id,
		MerchantId: user.MerchantId,
		Mobile:     user.Mobile,
		Username:   user.Username,
		RealName:   user.RealName,
		Role:       int32(user.Role),
		ExpiredAt:  timestamppb.New(expireAt),
		Token:      token,
	}
	cmd := u.redisClient.SetEX(ctx, authCacheKey(user.Id, appId), userAuth, time.Hour*24)
	if cmd.Err() != nil {
		return nil, cmd.Err()
	}
	return userAuth, nil
}

func (u *userAuthUsecase) Logout(ctx context.Context, id int32, appId int32) error {
	cmd := u.redisClient.Del(ctx, authCacheKey(id, appId))
	if cmd.Err() != nil {
		return errors.ErrorDBWrap(cmd.Err(), "redis get error")
	}
	return nil
}

func (u *userAuthUsecase) CheckLogin(ctx context.Context, token string, appId int32) (*domain.UserAuth, error) {
	id, app, err := decodeIdFromToken(token)
	if err != nil {
		return nil, err
	}
	if app != appId {
		return nil, pb.ErrorUnauthenticated("token无效或已过期")
	}
	cmd := u.redisClient.Get(context.Background(), authCacheKey(id, appId))
	if cmd.Err() != nil {
		if cmd.Err() == redis.Nil {
			return nil, pb.ErrorUnauthenticated("token无效或已过期")
		}
		return nil, errors.ErrorDBWrap(cmd.Err(), "redis get error")
	}
	var do domain.UserAuth
	err = cmd.Scan(&do)
	if err != nil || do.Token != token {
		return nil, pb.ErrorUnauthenticated("token无效或已过期")
	}
	return &do, nil
}

func authCacheKey(id int32, appId int32) string {
	return fmt.Sprintf("ut_%d_%d", appId, id)
}

func checkPassword(inputPassword, dbPassword, salt string) bool {
	hash := hmac.New(sha256.New, []byte(salt))
	hash.Write([]byte(inputPassword))
	password, _ := hex.DecodeString(dbPassword)
	return hmac.Equal(hash.Sum(nil), password)
}

func encodePassword(password, salt string) string {
	hash := hmac.New(sha256.New, []byte(salt))
	hash.Write([]byte(password))
	return hex.EncodeToString(hash.Sum(nil))
}

func generateToken(id int32, appId int32, expireAt time.Time) string {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":     strconv.Itoa(int(id)),
		"app_id": strconv.Itoa(int(appId)),
		"exp":    expireAt.Unix(),
	})
	s, _ := token.SignedString([]byte("123"))
	return s
}

func decodeIdFromToken(token string) (int32, int32, error) {
	t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
		if token.Method != jwt.SigningMethodHS256 {
			return nil, ErrInvalidToken
		}
		return []byte("123"), nil
	})
	if err != nil {
		return 0, 0, ErrInvalidToken
	}
	if c, ok := t.Claims.(jwt.MapClaims); ok {
		err = c.Valid()
		if err != nil {
			return 0, 0, err
		}
		if _, ok := c["id"]; !ok {
			return 0, 0, ErrInvalidToken
		} else if _, ok := c["app_id"]; !ok {
			return 0, 0, ErrInvalidToken
		}
		id, _ := strconv.Atoi(c["id"].(string))
		appId, _ := strconv.Atoi(c["app_id"].(string))
		return int32(id), int32(appId), nil
	}
	return 0, 0, ErrInvalidToken
}

func generateSalt(mobile string) string {
	m := md5.New()
	m.Write([]byte(fmt.Sprintf("%d:%s", time.Now().Unix(), mobile)))
	return hex.EncodeToString(m.Sum(nil))
}
